/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.sentry;

import io.sentry.util.CollectionUtils;

import java.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;

/**
 * Series of application events
 *
 * @since 2021-06-02
 **/
public final class Breadcrumb implements Cloneable, IUnknownPropertiesConsumer {
    /**
     * A timestamp representing when the breadcrumb occurred.
     */
    private final @NotNull Date timestamp;

    /**
     * If a message is provided, its rendered as text and the whitespace is preserved.
     */
    private @Nullable String message;

    /**
     * The type of breadcrumb.
     */
    private @Nullable String type;

    /**
     * Data associated with this breadcrumb.
     */
    private @NotNull Map<String, @NotNull Object> data = new ConcurrentHashMap<>();

    /**
     * Dotted strings that indicate what the crumb is or where it comes from.
     */
    private @Nullable String category;

    /**
     * The level of the event.
     */
    private @Nullable SentryLevel level;

    /**
     * the unknown fields of breadcrumbs, internal usage only
     */
    public  @Nullable Map<String, Object> unknown;

    /**
     * Breadcrumb ctor
     *
     * @param timestamp the timestamp
     */
    public Breadcrumb(final @NotNull Date timestamp) {
        this.timestamp = (Date) timestamp.clone();
    }

    /**
     * Breadcrumb ctor
     */
    public Breadcrumb() {
        this(DateUtils.getCurrentDateTime());
    }

    /**
     * Breadcrumb ctor
     *
     * @param message the message
     */
    public Breadcrumb(@Nullable String message) {
        this();
        this.message = message;
    }

    /**
     * Creates HTTP breadcrumb.
     *
     * @param url - the request URL
     * @param method - the request method
     * @return the breadcrumb
     */
    public static @NotNull Breadcrumb http(final @NotNull String url, final @NotNull String method) {
        final Breadcrumb breadcrumb = new Breadcrumb();
        breadcrumb.setType("http");
        breadcrumb.setCategory("http");
        breadcrumb.setData("url", url);
        breadcrumb.setData("method", method.toUpperCase(Locale.ROOT));
        return breadcrumb;
    }

    /**
     * Creates HTTP breadcrumb.
     *
     * @param url - the request URL
     * @param method - the request method
     * @param code - the code result. Code can be null when http request did not finish or ended with
     * network error
     *
     * @return the breadcrumb
     */
    public static @NotNull Breadcrumb http(
        final @NotNull String url, final @NotNull String method, final @Nullable Integer code) {
        final Breadcrumb breadcrumb = http(url, method);
        if (code != null) {
            breadcrumb.setData("status_code", code);
        }
        return breadcrumb;
    }

    /**
     * Returns the Breadcrumb's timestamp
     *
     * @return the timestamp
     */
    @SuppressWarnings({"JdkObsolete", "JavaUtilDate"})
    public @NotNull Date getTimestamp() {
        return (Date) timestamp.clone();
    }

    /**
     * Returns the message
     *
     * @return the message
     */
    public @Nullable String getMessage() {
        return message;
    }

    /**
     * Sets the message
     *
     * @param message the message
     */
    public void setMessage(@Nullable String message) {
        this.message = message;
    }

    /**
     * Returns the type
     *
     * @return the type
     */
    public @Nullable String getType() {
        return type;
    }

    /**
     * Sets the type
     *
     * @param type the type
     */
    public void setType(@Nullable String type) {
        this.type = type;
    }

    /**
     * Returns the data map
     *
     * @return the data map
     */
    @ApiStatus.Internal
    @NotNull
    public Map<String, Object> getData() {
        return data;
    }

    /**
     * Returns the value of data[key] or null
     *
     * @param key the key
     * @return the value or null
     */
    @Nullable
    public Object getData(final @NotNull String key) {
        return data.get(key);
    }

    /**
     * Sets an entry to the data's map
     *
     * @param key the key
     * @param value the value
     */
    public void setData(@NotNull String key, @NotNull Object value) {
        data.put(key, value);
    }

    /**
     * Removes an entry from the data's map
     *
     * @param key the key
     */
    public void removeData(@NotNull String key) {
        data.remove(key);
    }

    /**
     * Returns the category
     *
     * @return the category
     */
    public @Nullable String getCategory() {
        return category;
    }

    /**
     * Sets the category
     *
     * @param category the category
     */
    public void setCategory(@Nullable String category) {
        this.category = category;
    }

    /**
     * Returns the SentryLevel
     *
     * @return the level
     */
    public @Nullable SentryLevel getLevel() {
        return level;
    }

    /**
     * Sets the level
     *
     * @param level the level
     */
    public void setLevel(@Nullable SentryLevel level) {
        this.level = level;
    }

    /**
     * Sets the unknown fields, internal usage only
     *
     * @param unknowns the unknown's map
     */
    @ApiStatus.Internal
    @Override
    public void acceptUnknownProperties(@NotNull Map<String, Object> unknowns) {
        this.unknown = new ConcurrentHashMap<>(unknowns);
    }

    /**
     * Returns the unknown's map, internal usage only
     *
     * @return the unknown map
     */
    @TestOnly
    @Nullable
    Map<String, Object> getUnknown() {
        return unknown;
    }

    /**
     * Clones the breadcrumb aka deep copy
     *
     * @return the cloned breadcrumb
     * @throws CloneNotSupportedException if a breadcrumb is not cloneable
     */
    @Override
    public @NotNull Breadcrumb clone() throws CloneNotSupportedException {
        final Breadcrumb clone = (Breadcrumb) super.clone();

        final Map<String, Object> dataCopy = CollectionUtils.newConcurrentHashMap(this.data);
        if (dataCopy != null) {
            clone.data = dataCopy;
        }
        clone.unknown = CollectionUtils.newConcurrentHashMap(unknown);

        final SentryLevel levelRef = level;
        clone.level =
            levelRef != null ? SentryLevel.valueOf(levelRef.name().toUpperCase(Locale.ROOT)) : null;

        return clone;
    }
}
